import { encrypt, decrypt } from "@utils/crypto";
import { isBoolean } from "@/utils";
import { zWarn } from "../console";
import { getGlobalEnv, appVersion, isPrd } from "../constants";

/* change prefix or encrypt-secret will invalidate the storage cache. */
export interface ZStorageOption {
  prefix: string;
  /* use to join prefix and key, default '_' */
  joint?: string;
  /* by default, ${window.localStorage} */
  storage?: Storage;
  /**
   * a normal number means milliseconds to invalidate the value,
   * while `true`, `0`, `Infinity` and negative value mean never.
   * `false` means to disable `value-wrapping`.
   */
  expires?: number | boolean;
  /* by default, enabled in production */
  enableCrypt?: boolean;
}

/* `value-wrapping` */
interface ZStorageItemWrapped<T = any> {
  // value
  v: T;
  // expires
  e: number;
  // created time
  c: number;
}

function createStorage(option: ZStorageOption) {
  const {
    prefix,
    joint = "_",
    expires = 0,
    storage = window.localStorage,
    enableCrypt = isPrd,
  } = option;

  const wrapKey = (key: string) => [prefix, key].join(joint);
  const disableWrapValue = expires === false;
  const defaultExpires = isBoolean(expires) ? 0 : expires;
  const wrapValue = <T = any>(
    value: T,
    exp = defaultExpires
  ): T | ZStorageItemWrapped<T> => {
    if (disableWrapValue) return value;
    return {
      v: value,
      e: exp > 0 ? exp : 0,
      c: Date.now(),
    };
  };

  function set(key: string, value: any, expires = defaultExpires) {
    const wrappedValue = wrapValue(value, expires);
    storage.setItem(
      wrapKey(key),
      enableCrypt ? encrypt(wrappedValue) : JSON.stringify(wrappedValue)
    );
  }

  function invalidate(key: string) {
    set(wrapKey(key), null, 0);
  }

  function get<R = any>(key: string): R | null | undefined {
    const wrappedKey = wrapKey(key);
    const wrappedValue = storage.getItem(wrappedKey);
    if (wrappedValue) {
      let value;
      try {
        value = enableCrypt ? decrypt(wrappedValue) : JSON.parse(wrappedValue);
      } catch {
        invalidate(key);
        return null;
      }
      if (disableWrapValue) return value;
      const { v, e, c } = value as ZStorageItemWrapped;
      if (e > 0 && Date.now() - c > e) {
        invalidate(key);
        return null;
      }
      return v;
    }
  }

  function clear() {
    for (let i = 0; i < storage.length; i++) {
      const key = storage.key(i);
      if (key?.startsWith(prefix)) {
        storage.removeItem(key);
      }
    }
  }

  function remove(key: string) {
    if (key?.startsWith(prefix)) {
      storage.removeItem(key);
    } else {
      zWarn(`Can only remove item starts with ${prefix}`);
    }
  }

  return {
    set,
    get,
    clear,
    remove,
    invalidate,
  };
}

const { appTitle } = getGlobalEnv();

/* It won't be changed as the version is upgraded or time passes */
const baseStorageOption: ZStorageOption = {
  prefix: `${appTitle}__${isPrd ? "PRD" : "DEV"}`,
  expires: false,
};

export const baseStorage = createStorage(baseStorageOption);

const freshStorageOption: ZStorageOption = {
  prefix: `${appTitle}__${appVersion}__${isPrd ? "PRD" : "DEV"}`,
};

export const freshStorage = createStorage(freshStorageOption);
